﻿using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using SDCloudSolution.SDCloud.DataLayer.ModelData.Interfaces;
using SDCloudSolution.SDCloud.DataLayer.MongoDb;
using SDCloudSolution.SDCloud.DataLayer.MongoDb.Interfaces;
using SDCloudSolution.SDCloud.Entities.Models.Data;

namespace SDCloudSolution.SDCloud.DataLayer.ModelData
{
    public class MongoModelExecutionResultDataProvider : BaseMongoDataProvider, IModelExecutionResultDataProvider
    {
        public MongoModelExecutionResultDataProvider(ISdcloudMongoClient client, ISdCloudMongoDbSettingsProvider settings)
            : base(client, settings.ModelResultDataBaseName)
        {
            
        }

        protected IMongoCollection<BsonDocument> GetCollection()
        {
            return GetDatabase().GetCollection<BsonDocument>("model-result");
        }

        protected IMongoCollection<BsonDocument> GetTimeFrameCollection()
        {
            return GetDatabase().GetCollection<BsonDocument>("model-result-data");
        }

        public async Task<ModelResultDataEntity> GetResultsDataByModelExecutionIdAsync(int modelExecutionId)
        {
            var result = await GetModelResultDataWithoutTimeFrames(modelExecutionId);
            if (result != null)
            {
                var timeFrames = await FindTimeFrames(GetFilterForTimeFrames(modelExecutionId)).ToListAsync();
                result.TimeFrames = timeFrames.Select(document => document.ToTimeFrameEntity()).ToArray();
            }

            return result;
        }

        public async Task<ModelResultDataEntity> GetResultsDataByModelExecutionIdAsync(int modelExecutionId, int framesSkipCount, int framesLimitCount)
        {
            var result = await GetModelResultDataWithoutTimeFrames(modelExecutionId);
            if (result != null)
            {
                var timeFrames = await FindTimeFrames(GetFilterForTimeFrames(modelExecutionId))
                        .Skip(framesSkipCount)
                        .Limit(framesLimitCount)
                        .ToListAsync();

                result.TimeFrames = timeFrames.Select(document => document.ToTimeFrameEntity()).ToArray();
            }

            return result;
        }

        public async Task<ModelResultDataEntity> GetResultsDataByModelExecutionIdWithTimeFilter(int modelExecutionId, double? timeStart, double? timeEnd)
        {
            var result = await GetModelResultDataWithoutTimeFrames(modelExecutionId);
            if (result != null)
            {
                var filters = GetRangeFilter(MongoDbConverter.MongoFieldNames.ModelResultTimeFrame.Time, timeStart, timeEnd);

                var timeFrames = await FindTimeFrames(GetFilterForTimeFrames(modelExecutionId, filters)).ToListAsync();
                result.TimeFrames = timeFrames.Select(document => document.ToTimeFrameEntity()).ToArray();
            }

            return result;
        }

        private async Task<ModelResultDataEntity> GetModelResultDataWithoutTimeFrames(int modelExecutionId)
        {
            var filter = GetFilterByModelExecutionId(modelExecutionId);

            var documents = await GetCollection().Find(filter).Limit(1).ToListAsync();

            return documents.FirstOrDefault()?.ToResultDataEntity();
        }

        private IFindFluent<BsonDocument, BsonDocument> FindTimeFrames(FilterDefinition<BsonDocument> filter)
        {
            return GetTimeFrameCollection().Find(filter);
        }

        public Task InsertAsync(ModelResultDataEntity modelResultData)
        {
            var timeFrameInsertionTask = InsertAsync(modelResultData.TimeFrames);
            var modelResultInsertionTask = GetCollection().InsertOneAsync(modelResultData.ToBsonDocument());

            return Task.WhenAll(timeFrameInsertionTask, modelResultInsertionTask);
        }

        public Task InsertAsync(IEnumerable<ModelResultTimeFrameEntity> timeFrameEntities)
        {
            return GetTimeFrameCollection().InsertManyAsync(timeFrameEntities.Select(timeFrame => timeFrame.ToBsonDocument()));
        }

        private IEnumerable<FilterDefinition<BsonDocument>> GetRangeFilter(FieldDefinition<BsonDocument> fieldDefinition, double? start, double? end)
        {
            var result = new List<FilterDefinition<BsonDocument>>();

            if (start.HasValue)
            {
                result.Add(Builders<BsonDocument>.Filter.Gte(MongoDbConverter.MongoFieldNames.ModelResultTimeFrame.Time, start.Value));
            }

            if (end.HasValue)
            {
                result.Add(Builders<BsonDocument>.Filter.Lt(MongoDbConverter.MongoFieldNames.ModelResultTimeFrame.Time, end.Value));
            }

            return result;
        }

        private FilterDefinition<BsonDocument> GetFilterForTimeFrames(int modelExecutionId)
        {
            return Builders<BsonDocument>.Filter.Eq(MongoDbConverter.MongoFieldNames.ModelResultTimeFrame.ModelExecutionId, modelExecutionId);
        }

        private FilterDefinition<BsonDocument> GetFilterForTimeFrames(int modelExecutionId, IEnumerable<FilterDefinition<BsonDocument>> filters)
        {
            var filterDefinitions = new List<FilterDefinition<BsonDocument>>
            {
                Builders<BsonDocument>.Filter.Eq(MongoDbConverter.MongoFieldNames.ModelResultTimeFrame.ModelExecutionId, modelExecutionId)
            };
            filterDefinitions.AddRange(filters);

            return Builders<BsonDocument>.Filter.And(filterDefinitions);
        }

        private FilterDefinition<BsonDocument> GetFilterByModelExecutionId(int modelExecutionId)
        {
            return Builders<BsonDocument>.Filter.Eq(
                $"{MongoDbConverter.MongoFieldNames.ModelResultData.Metadata}.{MongoDbConverter.MongoFieldNames.ModelExecutionMetadata.ModelExecutionId}", modelExecutionId
            );
        }
    }
}
